package com.naszetatry.logic;

import java.security.NoSuchAlgorithmException;
import java.sql.*;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.naszetatry.beans.User;
import com.naszetatry.encryption.MD5;
import com.naszetatry.exception.*;

public class UserLogic implements DatabaseConstants {
	
	/**
	 * Minimal users login length.
	 */
	public static final int LOGIN_MIN = 3;
	/**
	 * Maximal users login length.
	 */
	public static final int LOGIN_MAX = 20;
	/**
	 * Minimal users password length.
	 */
	public static final int PASS_MIN = 6;
	/**
	 * Maximal users password length.
	 */
	public static final int PASS_MAX = 20;
	/**
	 * Maximal mail length.
	 */
	public static final int MAIL_MAX = 45;
	
	/**
	 * <p>
	 * Inserts new user record to database.
	 * Use only for the first save (register). To edit record
	 * use <code>update</code> function.
	 * User login and mail need to be unique.
	 * </p>
	 * @param user
	 * - user object
	 * @throws LoginAlreadyExistsException
	 * @throws MailAlreadyExistsException
	 * @throws InvalidLoginException 
	 * @throws InvalidPasswordException 
	 * @throws InvalidMailException 
	 * @see UserLogic#update(User)
	 */
	public static void register(User user) throws LoginAlreadyExistsException, MailAlreadyExistsException,
									InvalidLoginException, InvalidPasswordException, InvalidMailException {
		
		if(!isLoginValid(user.getLogin())) throw new InvalidLoginException();
		if(!isPasswordValid(user.getPassword())) throw new InvalidPasswordException();
		if(!isMailValid(user.getMail())) throw new InvalidMailException();
		
		/*
		 * Checks if user have activation key.
		 * Saving user without it is an error.
		 */
		if(user.getActivationKey() == null) throw new NullPointerException("activationKey");
		
		/*
		 * Encrypting password
		 */
		try {
			user.setPassword(MD5.encrypt(user.getPassword()));
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException(e);
		}
		
		try {
			Class.forName("com.mysql.jdbc.Driver");
		} catch (ClassNotFoundException e) {
			throw new RuntimeException(e);
		}
		
		/*
		 * Main database operations
		 */
		Connection connect = null;
		PreparedStatement preparedStatement = null;
		ResultSet resultSet = null;
		try {
			connect = DriverManager.getConnection(DB);							
			
			/*
			 * Check if login or mail is in database already
			 */
			preparedStatement = connect.prepareStatement("select login, mail from mac_naszetatry.user where " +
					"login = ? or mail = ?");
			
			preparedStatement.setString(1, user.getLogin());
			preparedStatement.setString(2, user.getMail());
			
			resultSet = preparedStatement.executeQuery();
			if(resultSet != null) {
				while(resultSet.next()) {
					if(user.getLogin().equals(resultSet.getString("login")))
						throw new LoginAlreadyExistsException();
					if(user.getMail().equals(resultSet.getString("mail")))
						throw new MailAlreadyExistsException();
				}
			}
			
			/*
			 * Add record to database
			 */
			preparedStatement = connect.prepareStatement(
					"INSERT INTO `mac_naszetatry`.`user` (`login`, `password`, `mail`, `key`, `isActive`) " +
					"VALUES (?, ?, ?, ?, 0)");
				
			preparedStatement.setString(1, user.getLogin());
			preparedStatement.setString(2, user.getPassword());
			preparedStatement.setString(3, user.getMail());
			preparedStatement.setString(4, user.getActivationKey());
			
			preparedStatement.executeUpdate();
				
		} catch (SQLException e) {
			throw new RuntimeException(e);
		} finally {
			try {
				connect.close();
				preparedStatement.close();
				resultSet.close();
			} catch (SQLException ex) {
				throw new RuntimeException(ex);
			}
		}
	}
	
	/**
	 * Used to activate users account. It changes isActive column
	 * in database from 0 to 1. In other case the proper exception
	 * will be thrown.
	 * @param user
	 * - user that will be activated
	 * @throws InvalidLoginException
	 * @throws InvalidActivationKeyException
	 * @throws AlreadyActivatedException
	 */
	public static void activate(User user) throws InvalidLoginException, InvalidActivationKeyException,
									AlreadyActivatedException {
		
		User baseUser = null;
		
		/*
		 * Validate user fields
		 */
		if(!isLoginValid(user.getLogin())) throw new InvalidLoginException();
		
		Connection connect = null;
		PreparedStatement preparedStatement = null;
		ResultSet rs = null;
		try {
			connect = DriverManager.getConnection(DB);
			
			preparedStatement = connect.prepareStatement(
					"SELECT * FROM `mac_naszetatry`.`user` WHERE `login` = ?");
				
			preparedStatement.setString(1, user.getLogin());
				
			rs = preparedStatement.executeQuery();
			
			if(rs.next()) {
				baseUser = new User();
				initUser(baseUser, rs);
			} else {
				throw new InvalidLoginException();
			}
			
			//validate
			if(!user.getActivationKey().equals(baseUser.getActivationKey()))
				throw new InvalidActivationKeyException();
			if(user.isActive())
				throw new AlreadyActivatedException();
			
			//activate
			preparedStatement = connect.prepareStatement(
					"UPDATE `mac_naszetatry`.`user` SET `isActive` = 1 " +
					"WHERE `login` = ?");
				
			preparedStatement.setString(1, user.getLogin());
			
			preparedStatement.executeUpdate();
			
		} catch (SQLException e) {
			throw new RuntimeException(e);
		} finally {
			try {
				connect.close();
				preparedStatement.close();
				rs.close();
			} catch (SQLException ex) {
				throw new RuntimeException(ex);
			}
		}
	}
	
	/**
	 * Retrieves all data from database for specified user.
	 * @param user
	 * - user that will be logged in. It should have defined
	 * proper login and password. User must be also activated in
	 * database (<code>isActive = 1</code>). In other case the proper exception
	 * will be thrown.
	 * @return
	 * User with all parameters defined.
	 * @throws InvalidLoginException
	 * @throws InvalidPasswordException
	 * @throws NotActivatedException
	 */
	public static User login(User user) throws InvalidLoginException, InvalidPasswordException,
								NotActivatedException {
		User baseUser = null;
		
		if(!isLoginValid(user.getLogin())) throw new InvalidLoginException();
		if(!isPasswordValid(user.getPassword())) throw new InvalidPasswordException();
		
		Connection connect = null;
		PreparedStatement preparedStatement = null;
		ResultSet rs = null;
		try {
			connect = DriverManager.getConnection(DB);

			preparedStatement = connect.prepareStatement(
			"SELECT * FROM `mac_naszetatry`.`user` WHERE `login` = ?");
			
			preparedStatement.setString(1, user.getLogin());
			
			rs = preparedStatement.executeQuery();
			
			if(rs.next()) {
				baseUser = new User();
				initUser(baseUser, rs);
			} else {
				throw new InvalidLoginException();
			}
			
		} catch (SQLException e) {
			throw new RuntimeException(e);
		} finally {
			try {
				connect.close();
				preparedStatement.close();
				if(rs != null) rs.close();
			} catch (SQLException ex) {
				throw new RuntimeException(ex);
			}
		}
		
		if(!baseUser.isActive()) throw new NotActivatedException();
		
		/*
		 * Encrypting password
		 */
		try {
			user.setPassword(MD5.encrypt(user.getPassword()));
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException(e);
		}
		
		if(!baseUser.getPassword().equals(user.getPassword())) throw new InvalidPasswordException();
		
		return baseUser;
	}
	
	/**
	 * Can be used to update users mail and password in database.
	 * If there's no need to update some information (i.e. mail)
	 * this parameter in <code>changed</code> user should be 
	 * specified as <code>null</code>. In case of some wrong data
	 * exception can be thrown.
	 * @param old
	 * User object with valid
	 * <li>login
	 * <li>old password
	 * @param changed
	 * User object with valid
	 * <li>mail (or <code>null</code>
	 * <li>password (or <code>null</code>
	 * @throws InvalidPasswordException
	 * @throws InvalidMailException
	 * @throws InvalidLoginException
	 */
	public static void update(User old, User changed) throws InvalidPasswordException, InvalidMailException,
												InvalidLoginException {
				
		//check if new mail and pass are correct
		if(changed.getMail() != null && !"".equals(changed.getMail()) && !isMailValid(changed.getMail()))
			throw new InvalidMailException();
		if(changed.getPassword() != null && !"".equals(changed.getPassword()) && !isPasswordValid(changed.getPassword()))
			throw new InvalidPasswordException();
		
		Connection connect = null;
		PreparedStatement preparedStatement = null;
		ResultSet rs = null;
		try {
			connect = DriverManager.getConnection(DB);
													
			preparedStatement = connect.prepareStatement(
			"SELECT * FROM `mac_naszetatry`.`user` WHERE `login` = ?");
			
			preparedStatement.setString(1, old.getLogin());
			
			rs = preparedStatement.executeQuery();
			
			if(rs.next()) {
				try {
					if(!MD5.encrypt(old.getPassword()).equals(rs.getString("password")))
						throw new InvalidPasswordException();
				} catch (NoSuchAlgorithmException e) {
					throw new RuntimeException();
				}
				initUser(old, rs);
			} else {
				throw new InvalidLoginException();
			}
			
			//fill null values in changed
			if(changed.getMail() == null || "".equals(changed.getMail()))
				changed.setMail(old.getMail());
			if(changed.getPassword() == null || "".equals(changed.getPassword()))
				changed.setPassword(old.getPassword());
			
			//update changes
			String sql = "UPDATE `mac_naszetatry`.`user` SET mail = ?, password = ? "
						+ "WHERE login = ?";
			
			preparedStatement = connect.prepareStatement(sql);
			
			String pass;
			try {
				pass = MD5.encrypt(changed.getPassword());
			} catch (NoSuchAlgorithmException e) {
				throw new RuntimeException();
			}
			
			preparedStatement.setString(1, changed.getMail());
			preparedStatement.setString(2, pass);
			preparedStatement.setString(3, old.getLogin());
			
			preparedStatement.executeUpdate();
			
		} catch (SQLException e) {
			throw new RuntimeException(e);
		} finally {
			try {
				connect.close();
				preparedStatement.close();
				if(rs != null) rs.close();
			} catch (SQLException ex) {
				throw new RuntimeException(ex);
			}
		}
	}
	

	/**
	 * Used to change users password. Can be used in cases when
	 * user forgot his password and wants to generate new one.
	 * @param changed
	 * - User object with valid:
	 * <li>login
	 * <li>password
	 * @throws InvalidPasswordException
	 * @throws InvalidLoginException
	 */
	public static void updatePassword(User changed) throws InvalidPasswordException,
												InvalidLoginException {
				
		if(!isPasswordValid(changed.getPassword()))
			throw new InvalidPasswordException();
		
		Connection connect = null;
		PreparedStatement preparedStatement = null;
		ResultSet rs = null;
		try {
			connect = DriverManager.getConnection(DB);
			
			//update changes
			String sql = "UPDATE `mac_naszetatry`.`user` SET password = ? "
						+ "WHERE login = ?";
			
			preparedStatement = connect.prepareStatement(sql);
			
			String pass;
			try {
				pass = MD5.encrypt(changed.getPassword());
			} catch (NoSuchAlgorithmException e) {
				throw new RuntimeException();
			}
			
			preparedStatement.setString(1, pass);
			preparedStatement.setString(2, changed.getLogin());
			
			preparedStatement.executeUpdate();
			
		} catch (SQLException e) {
			throw new RuntimeException(e);
		} finally {
			try {
				connect.close();
				preparedStatement.close();
				if(rs != null) rs.close();
			} catch (SQLException ex) {
				throw new RuntimeException(ex);
			}
		}
	}
	
	/**
	 * Function used to generate new users password if
	 * he requests for it (forgotten password).
	 * @param user
	 * - user object. Should contain valid login.
	 * If not SQLException will be thrown.
	 * @return
	 * User object with newly generated (not encrypted) password.
	 * Other user fields will be not changed.
	 * 
	 * @deprecated
	 * Not used, replaced by {@link UserLogic#updatePassword(User)}
	 */
	@Deprecated
	public static User generatePassword(User user) {
		
		user.setPassword(generateNewPassword());
		String pass;
		try {
			pass = MD5.encrypt(user.getPassword());
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException(e);
		}
		
		Connection connect = null;
		PreparedStatement preparedStatement = null;
		try {
			connect = DriverManager.getConnection(DB);
												
			preparedStatement = connect.prepareStatement(
					"UPDATE `mac_naszetatry`.`user` SET password = ? " +
					"WHERE `login` = ?");
			
			preparedStatement.setString(1, pass);
			preparedStatement.setString(2, user.getLogin());
			
			preparedStatement.executeUpdate();
			
		} catch (SQLException e) {
			throw new RuntimeException(e);
		} finally {
			try {
				connect.close();
				preparedStatement.close();
			} catch (SQLException ex) {
				throw new RuntimeException(ex);
			}
		}
		
		return user;
	}
	
	/**
	 * Returns list of users with logins that start up
	 * on specified string.
	 * @param login
	 * - string that defines the logins begin.
	 * @return
	 * List of user logins fulfilling search criteria.
	 * Can be empty.
	 */
	public static List<String> loadUserLogins(String login) {
		List<String> list = new ArrayList<String>();
		
		if(!isRegularExpression(login)) return list;
		
		Connection connect = null;
		PreparedStatement preparedStatement = null;
		ResultSet rs = null;
		try {
			connect = DriverManager.getConnection(DB);
										
			preparedStatement = connect.prepareStatement(
			"SELECT login FROM `mac_naszetatry`.`user` WHERE `login` like ?");
			
			login += "%";
			preparedStatement.setString(1, login);
			
			rs = preparedStatement.executeQuery();
			
			while(rs.next()) {
				String baseLogin = rs.getString("login");
				list.add(baseLogin);
			}
			
		} catch (SQLException e) {
			throw new RuntimeException(e);
		} finally {
			try {
				connect.close();
				preparedStatement.close();
				if(rs != null) rs.close();
			} catch (SQLException ex) {
				throw new RuntimeException(ex);
			}
		}
		
		return list;
	}
	
	private static void initUser(User u, ResultSet rs) {		
		try {
			u.setLogin(rs.getString("login"));
			u.setPassword(rs.getString("password"));
			u.setMail(rs.getString("mail"));
			u.setActivationKey(rs.getString("key"));
			if(rs.getInt("isActive") > 0) u.setActive(true);
			else u.setActive(false);
		} catch(SQLException e) {
			u = null;
		}
	}
	
	/**
	 * Generates new password and returns it as a string.
	 * @return
	 * Password with length between PASS_MIN and PASS_MAX,
	 * contains small and big letters and at least one digit.
	 */
	public static String generateNewPassword() {
		String pass = "";
		Random r = new Random();
		int seed = (int) new GregorianCalendar().getTimeInMillis();
		int length = PASS_MIN + r.nextInt(seed) % (PASS_MAX-PASS_MIN);
		int digits = 1 + r.nextInt(seed)%(length/3);
		
		boolean number = false;	//what to generate digit/letter
		int i = 0;	//digits
		int j = 0;	//letters
		
		while(i+j < length) {
			int n = r.nextInt(seed)%2;
			if(n != 0) number = true;
			else number = false;
			
			if(i == digits) number = false;
			if(j == length - digits) number = true;
			
			char c;
			
			if(number) {
				c = Character.forDigit(r.nextInt(seed)%10, 10);
				pass += c;
				i++;
			} else {
				do {
					c = (char) (67 + r.nextInt(seed)% (56));
				} while(!Character.isLetter(c));
				pass += c;
				j++;
			}
		}
		
		return pass;
	}
	
	private static boolean isLoginValid(String login) {
		boolean valid = true;
		valid &= isRegularExpression(login);
		valid &= login.length() >= LOGIN_MIN;
		valid &= login.length() <= LOGIN_MAX;
		return valid;
	}
	
	private static boolean isPasswordValid(String pass) {
		boolean valid = true;
		valid &= isRegularExpression(pass);
		valid &= pass.length() >= PASS_MIN;
		valid &= pass.length() <= PASS_MAX;
		valid &= containsNumber(pass);
		return valid;
	}
	
	private static boolean isMailValid(String mail) {
		
		if(mail.length() > MAIL_MAX) return false;
		
		int i = mail.indexOf('@');
		if(i < 0) return false;
		
		String str = mail.substring(0, i-1);
		if(str.indexOf('.') == 0) return false;
		if(!isRegularExpression(str)) return false;
		
		str = mail.substring(i+1, mail.length());
		if(str.indexOf('.') == 0) return false;
		if(str.endsWith(".")) return false;
		if(!isRegularExpression(str) || !str.contains(".")) return false;
		
		return true;
	}
	
	private static boolean isRegularExpression(String string) {
		
		if(string == null || "".equals(string)) return false;
		Pattern p = Pattern.compile("[^a-z0-9 .]", Pattern.CASE_INSENSITIVE);
		Matcher m = p.matcher(string);
		return !m.find();
	}
	
	private static boolean containsNumber(String string) {
		
		Pattern p = Pattern.compile("[0-9]", Pattern.CASE_INSENSITIVE);
		Matcher m = p.matcher(string);
		return m.find();
	}
	
	/**
	 * Test
	 * @param args
	 */
	@Deprecated
	public static void main(String[] args) {
		
		System.out.println("---------------------------------");
//		User user = new User();
//		user.setLogin("aaab");
//		user.setPassword("koteczek1");
//		user.setMail("kottek@gmail.com");
//		user.setActivationKey("krowa222");
//		try {
//			register(user);
//		} catch(Exception ex) {
//			ex.printStackTrace();
//		}
		
//		for(int i = 0; i < 50; i++) {
//			System.out.println(generateNewPassword());
//		}
		
//		String login = "borniusze";
//		List<String> users = UserLogic.loadUserLogins(login);
//		Iterator<String> it = users.iterator();
//		while(it.hasNext()) {
//			System.out.println(it.next());
//		}
		
		User old = new User();
		old.setLogin("inborn");
		old.setPassword("koziEj977");
		
		User changed = new User();
		changed.setPassword("koziEj77");
		changed.setMail("krowa.nie.je.miesa@gmail.com");
		
		try {
			update(old, changed);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
